/*
 MetaCrypt is a class used to compress bit arrays, int arrays, or strings, and decompress them later. This would typically be used in a 
 networking context for speed and some encryption. */
MetaCrypt {
	
	/*
	Compression of bit arrays is very high, upwards to 30:1
	
	MetaCrypt.encode
	@param bitArray the array of bits to be encoded. This can be of an arbitrary length.
	@return Multidimensional array of packed 32bit integers
	*/
	*encodeBitArray {
		|bitArray|
		var packedIntArray,numIndexes;
		//bitArray.postln;
		// Determine the number of 32bit arrays needed to divide the incoming bit array into
		numIndexes = (bitArray.size/30).ceil(1);
		packedIntArray = [];
		
		// Divide the contents of the input bit array among the multidimensional packedIntArray
		numIndexes.do {
			|i|
			packedIntArray = packedIntArray.add( bitArray.copyRange((0+(30*i)).asInteger,(29+(30*i)).asInteger) );
			packedIntArray[i] = packedIntArray[i].addFirst(1);
			packedIntArray[i] = packedIntArray[i].addFirst(1);
		};
		
		
		// Iterate through each dimension of the packedIntArray and Encode them as packed 32 bit integers
		numIndexes.do {
			|index|
			var packedInt = 0;
			
			// going from left to right, bitshift by the index and add it to the total
			packedIntArray[index].reverse.collect {
				|item,index2|	
				packedInt = packedInt + (item<<index2);
			};
			
			packedIntArray[index] = packedInt;
		};
		
		^packedIntArray;
	}
	
	/*
	MetaCrypt.decodeBitArray
	@param packedIntArray An array of packed integers (this is created using MetaCrypt.encodeBitArray)
	@param pad if the bit array is smaller than this amount the rest will be padded with 0's from the left
	@return a flattened array of bits that describe the integers
	*/
	*decodeBitArray {
		|packedIntArray|
		var bitArray,bitSize;
		bitArray = Array.fill(packedIntArray.size,{ [] });
		packedIntArray.collect {
			|int,index|
			bitSize = MetaCrypt.numberOfBits(int);
			bitSize.do {
				|index2|
				bitArray[index] = bitArray[index].add( ( int >> ( bitSize - index2 - 1 ) ) & 1 );
			};
			bitArray[index].removeAt(0);
			bitArray[index].removeAt(0);
		};
		
		bitArray = bitArray.flat;
		
		^bitArray;
	}
	
	/*
	MetaCrypt.numberOfBits
	@param int an integer that you wish to find the minimum number of bits necessary to describe it
	@return int the bit number of bits that describe the number
	*/
	*numberOfBits {
		|int|
		var numberOfBits = 0;
		while({ int != 0 },
		{
			numberOfBits = numberOfBits + 1;
			// +>> is the logical right bit shift. In other languages this is >>>, used here to support negative numbers
			int = int+>>1;
		});
		^numberOfBits;
	}
	
	
	/*
	
	Int Array pack/unpack tests, compression rate is variable, the smaller the numbers the greater the compression. Additionally negative numbers 
	are automatically 32 bits therefor avoid them wherever possible. The compression rate can be anywhere from 4:1 to over unity (under poor conditions, 
	ie. high bit depth with large integers).
	
	This should really only be used for int arrays with numbers up to 25 bit ( 0 - 33554431 ), otherwise their sized coul actually increase
	ALSO Negative values are automatically 32 bit, thus negating any advantage compression, so avoid negative numbers whenever possible.
	
	Execute to list Bit Depth Ranges:
	
	(
		32.do {
			|i|
			("" ++ (i+1) ++ "-bit: " ++ MetaCrypt.encodeBitArray(Array.fill(i,{ 1 }))[0] ).postln;
		}
	)
	
	note: negative numbers of any range are expressed as 32 bit Integers in SC. if you want to use negative number with compression, push them to 
	bt unipolar before compression, and then adjust on the otherside after decompression.
	
	MetaCrypt.encodeIntArray
	@param intArray an integer array that you wish to find the minimum number of bits necessary to describe it.
	
	@param the maximum bit depth you want to encode to. If you know your ranges of numbers won't exceed a certain bit depth you can lower this number
	and gain a higher compression ratios. 
	
	@return packedIntArray the array of packed 32 bit integers holding the compressed information
	*/
	*encodeIntArray {
		|intArray,bitDepth=32|
		var packedIntArray,numIndexes,bitArray;
		bitArray = MetaCrypt.intArrayToBitArray(intArray,bitDepth);
		packedIntArray = MetaCrypt.encodeBitArray(bitArray);
		
		^packedIntArray;
	}
	
	/*
	This should really only be used for int arrays with numbers up to 25 bit ( 0 - 33554431 ), otherwise their sized coul actually increase
	ALSO Negative values are automatically 32 bit, thus negating any advantage compression, so avoid negative numbers whenever possible
	Technique: each number has a 6 bit header to indicate the bit depth of the int
	
	MetaCrypt.decodeIntArray
	@param int an integer that you wish to find the minimum number of bits necessary to describe it
	@return int the bit number of bits that describe the number
	*/
	*decodeIntArray {
		|packedIntArray,bitDepth=32|
		var bitArray,intArray;
		bitArray = MetaCrypt.decodeBitArray(packedIntArray);
		intArray = MetaCrypt.bitArrayToIntArray(bitArray,bitDepth);
		
		^intArray;
	}
	
	/*
	PRIVATE METHOD: DO NOT USE THIS OUTSIDE THE CLASS UNLESS YOU KNOW WHY TO USE IT
	MetaCrypt.intArrayToBitArray 
	@param intArray an array of integers to be converted into bits
	@return bitArray an array of bits that described the supplied array of integers, each integer is preceded by a 6-bit Bit Depth descriptor for the Integer
	*/
	*intArrayToBitArray {
		|intArray,bitDepth=32|
		var bitArray,bitSize,headerBits;
		bitArray = [];
		headerBits = MetaCrypt.numberOfBits(bitDepth);
		intArray.collect {
			|int,index|
			bitSize = MetaCrypt.numberOfBits(int);
			
			// Add a bit depth header of 6 bits (0-32)
			headerBits.do {
				|bitIndex|
				bitArray = bitArray.add( ( bitSize >> ( headerBits - bitIndex - 1 ) ) & 1);
			};
			
			// Encode the actual number
			bitSize.do {
				|index2|
				bitArray = bitArray.add( ( int >> ( bitSize - index2 - 1 ) ) & 1 );
			};
			
		};
				
		^bitArray;
	}
	
	/*
	PRIVATE METHOD: DO NOT USE THIS OUTSIDE THE CLASS UNLESS YOU KNOW WHY TO USE IT
	MetaCrypt.intArrayToBitArray 
	@param intArray an array of integers to be converted into bits
	@return bitArray an array of bits that described the supplied array of integers, each integer is preceded by a 6-bit Bit Depth descriptor for the Integer
	*/
	*intArrayToFixedBitArray {
		|intArray,bitDepth=32|
		var bitArray;
		bitArray = [];
		intArray.collect {
			|int,index|
			
			// Encode the actual number
			bitDepth.do {
				|index2|
				bitArray = bitArray.add( ( int >> ( bitDepth - index2 - 1 ) ) & 1 );
			};
			
		};
				
		^bitArray;
	}
	
	/*
	PRIVATE METHOD: DO NOT USE THIS OUTSIDE THE CLASS UNLESS YOU KNOW WHY TO USE IT
	MetaCrypt.bitArrayToIntArray 
	@param bitArray an array of bits that describe an array of integers, each integer is preceded by a 6-bit Bit Depth descriptor for the Integer
	
	@return intArray an array of integers to be converted from the supplied bit array
	*/
	*bitArrayToIntArray {
		|bitArray,bitDepth=32|
		var intArray,cursor,currentBitDepthHeader,currentBitDepth,currentIntegerBits,currentInteger,headerBits;
		intArray = [];
		cursor = 0;
		currentBitDepthHeader = [];
		currentBitDepth = 0;
		currentIntegerBits = [];
		currentInteger = 0;
		headerBits = MetaCrypt.numberOfBits(bitDepth);
		
		// Iterate through the bitArray
		while({ cursor < bitArray.size },
		{
			
			currentBitDepth = 0;
			currentInteger = 0;
			
			// Find the bit depth header
			currentBitDepthHeader = bitArray.copyRange(cursor,cursor+headerBits-1);
			
			// Extract the bit depth from the header
			// going from left to right, bitshift by the index and add it to the total
			currentBitDepthHeader.reverse.collect {
				|item,index2|	
				currentBitDepth = currentBitDepth + (item<<index2);
			};
			
			// Move through the bit depth header
			cursor = cursor + headerBits;
			
			// Get the actual integer out of the correct series of bits
			currentIntegerBits = bitArray.copyRange(cursor,cursor+currentBitDepth-1);
			
			// going from left to right, bitshift by the index and add it to the total
			currentIntegerBits.reverse.collect {
				|item,index2|	
				currentInteger = currentInteger + (item<<index2);
			};
			
			intArray = intArray.add(currentInteger);
			
			cursor = cursor + currentBitDepth;
		});
				
		^intArray;
	}
	
	/*
	PRIVATE METHOD: DO NOT USE THIS OUTSIDE THE CLASS UNLESS YOU KNOW WHY TO USE IT
	MetaCrypt.bitArrayToIntArray 
	@param bitArray an array of bits that describe an array of integers, each integer is preceded by a 6-bit Bit Depth descriptor for the Integer
	
	@return intArray an array of integers to be converted from the supplied bit array
	*/
	*fixedBitArrayToIntArray {
		|bitArray,bitDepth=32|
		var intArray,cursor,currentIntegerBits,currentInteger;
		intArray = [];
		cursor = 0;
		currentIntegerBits = [];
		currentInteger = 0;
		
		// Iterate through the bitArray
		while({ cursor < bitArray.size },
		{			
			currentInteger = 0;
			
			// Get the actual integer out of the correct series of bits
			currentIntegerBits = bitArray.copyRange(cursor,cursor+bitDepth-1);
			
			// going from left to right, bitshift by the index and add it to the total
			currentIntegerBits.reverse.collect {
				|item,index2|	
				currentInteger = currentInteger + (item<<index2);
			};
			
			intArray = intArray.add(currentInteger);
			
			cursor = cursor + bitDepth;
			//intArray.postln;
		});
				
		^intArray;
	}
	
	
	
	/* 
	MetaCrypt.encodeString
	
	@param string The string to be encoded 
	@param bitDepth Typical ascii characters are 7-bit, if you need more characters you can always increase the bit depth, although the higher the bit depth the 
	less effective the compression.
	
	Using 7-bit characters compression rates will be around 3:1 on average
	
	@return packedIntArray the array of packed 32 bit integers holding the compressed information
	*/
	*encodeString {
		|string,bitDepth = 7|
		var bitArray,packedIntArray;
		bitArray = MetaCrypt.intArrayToFixedBitArray(string.ascii,bitDepth);
		packedIntArray = MetaCrypt.encodeBitArray(bitArray);
		
		^packedIntArray;
	}
	
	/* 
	MetaCrypt.decodeString
	
	@ param packedIntArray the array of packed 32 bit integers holding the compressed information
	@param bitDepth Typical ascii characters are 7-bit, if you need more characters you can always increase the bit depth, although the higher the bit depth the 
	less effective the compression.
	
	@param string The string to be encoded 
	*/
	*decodeString {
		|packedIntArray,bitDepth = 7|
		var bitArray,intArray;
		bitArray = MetaCrypt.decodeBitArray(packedIntArray);
		intArray = MetaCrypt.fixedBitArrayToIntArray(bitArray,bitDepth);
		
		^intArray.asAscii;
	}
		
}